﻿namespace Webinator.Performance
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Threading;
    using System.Web.Script.Serialization;
    using ExtensionMethods;
    using Util;

    /// <summary>
    /// Contains methods for working with page captures and HAR files.
    /// </summary>
    public class HarToolbox
    {
        /// <summary>
        /// The web manager configuration.
        /// </summary>
        private readonly Config _config;

        /// <summary>
        /// Initializes a new instance of the <see cref="HarToolbox"/> class.
        /// </summary>
        /// <param name="webManager">
        /// The web manager.
        /// </param>
        public HarToolbox(IWebManager webManager)
        {
            _config = webManager.Config;
        }

        /// <summary>
        /// Parses the given HAR file.
        /// </summary>
        /// <param name="file">
        /// The file.
        /// </param>
        /// <param name="pageName">
        /// The page name.
        /// </param>
        /// <returns>
        /// A <c>PageInfo</c> object.
        /// </returns>
        public PageInfo ProcessHar(string file, string pageName)
        {
            var json = File.ReadAllText(file);
            var serializer = new JavaScriptSerializer { MaxJsonLength = 50000000 };
            serializer.RegisterConverters(new[] { new DynamicJsonConverter() });
            dynamic data = serializer.Deserialize(json, typeof(object));

            var size = 0;
            var requestsNotFromCache = 0;
            var nonCachedSize = 0;
            var pageError = false;
            var pageErrorCode = 0;

            // Timings
            var startTime = DateTime.MinValue;
            var endTime = DateTime.MinValue;
            var onLoad = 0;
            var onContentLoad = 0;
            int blocked = 0;
            int dns = 0;
            int connect = 0;
            int send = 0;
            int wait = 0;
            int receive = 0;
            int ssl = 0;

            // Statistics
            int html = 0;
            int javascript = 0;
            int css = 0;
            int image = 0;
            int flash = 0;
            int others = 0;
            int htmlSize = 0;
            int javascriptSize = 0;
            int cssSize = 0;
            int imageSize = 0;
            int flashSize = 0;
            int othersSize = 0;

            if (data.log.pages != null && data.log.pages[0] != null && data.log.pages[0].pageTimings != null)
            {
                onLoad = Convert.ToInt32(data.log.pages[0].pageTimings.onLoad);
                onContentLoad = Convert.ToInt32(data.log.pages[0].pageTimings.onContentLoad);
            }

            var requests = data.log.entries;
            int numberOfRequests = requests.Count;

            // Calculate timings, if anything happened more than 1000ms after everything else, then don't include in timings
            List<List<DateTime>> startEnd = new List<List<DateTime>>();
            foreach (var request in requests)
            {
                var requestStartTime = Convert.ToDateTime(request.startedDateTime);
                var requestTime = Convert.ToInt32(request.time);
                var requestEndTime = requestStartTime.AddMilliseconds(requestTime);

                startEnd.Add(new List<DateTime> { requestStartTime, requestEndTime });
            }

            bool allPass;
            do
            {
                allPass = true;

                for (int i = startEnd.Count - 1; i >= 0; i--)
                {
                    DateTime thisStart = startEnd[i][0];
                    bool acceptable = false;

                    foreach (var end in startEnd)
                    {
                        if (end[0] != thisStart)
                        {
                            if (thisStart.Subtract(end[1]).TotalMilliseconds < 1000)
                            {
                                acceptable = true;
                                break;
                            }
                        }
                    }

                    if (!acceptable)
                    {
                        startEnd.RemoveAt(i);
                        allPass = false;
                    }
                }
            } 
            while (!allPass);

            foreach (var timing in startEnd)
            {
                var requestStartTime = timing[0];
                var requestEndTime = timing[1];

                if (requestStartTime.CompareTo(startTime) < 0 || startTime == DateTime.MinValue)
                {
                    startTime = requestStartTime;
                }

                if (requestEndTime.CompareTo(endTime) > 0)
                {
                    endTime = requestEndTime;
                }
            }

            var totalTime = int.Parse(endTime.Subtract(startTime).TotalMilliseconds.ToString(CultureInfo.InvariantCulture));

            // Calculate everything else
            foreach (var request in requests)
            {
                var responseSize = Convert.ToInt32(request.response.bodySize);
                if (responseSize > 0)
                {
                    size += responseSize;
                }

                int temp = 0;
                if (request.timings.blocked != null)
                {
                    temp = Convert.ToInt32(request.timings.blocked);
                }

                if (temp > 0)
                {
                    blocked += temp;
                }

                temp = 0;
                if (request.timings.dns != null)
                {
                    temp = Convert.ToInt32(request.timings.dns);
                }

                if (temp > 0)
                {
                    dns += temp;
                }

                temp = 0;
                if (request.timings.connect != null)
                {
                    temp = Convert.ToInt32(request.timings.connect);
                }

                if (temp > 0)
                {
                    connect += temp;
                }

                temp = 0;
                if (request.timings.send != null)
                {
                    temp = Convert.ToInt32(request.timings.send);
                }

                if (temp > 0)
                {
                    send += temp;
                }

                temp = 0;
                if (request.timings.wait != null)
                {
                    temp = Convert.ToInt32(request.timings.wait);
                }

                if (temp > 0)
                {
                    wait += temp;
                }

                temp = 0;
                if (request.timings.receive != null)
                {
                    temp = Convert.ToInt32(request.timings.receive);
                }

                if (temp > 0)
                {
                    receive += temp;
                }

                temp = 0;
                if (request.timings.ssl != null)
                {
                    temp = Convert.ToInt32(request.timings.ssl);
                }

                if (temp > 0)
                {
                    ssl += temp;
                }

                var contentType = Convert.ToString(request.response.content.mimeType).ToLower();
                if (contentType.Contains("css"))
                {
                    css++;
                    if (responseSize > 0)
                    {
                        cssSize += responseSize;
                    }
                }
                else if (contentType.Contains("javascript") || contentType.Contains("js"))
                {
                    javascript++;
                    if (responseSize > 0)
                    {
                        javascriptSize += responseSize;
                    }
                }
                else if (contentType.Contains("html"))
                {
                    html++;
                    if (responseSize > 0)
                    {
                        htmlSize += responseSize;
                    }
                }
                else if (contentType.Contains("image"))
                {
                    image++;
                    if (responseSize > 0)
                    {
                        imageSize += responseSize;
                    }
                }
                else if (contentType.Contains("flash"))
                {
                    flash++;
                    if (responseSize > 0)
                    {
                        flashSize += responseSize;
                    }
                }
                else
                {
                    others++;
                    if (responseSize > 0)
                    {
                        othersSize += responseSize;
                    }
                }

                var status = Convert.ToInt32(request.response.status);
                if (status >= 400 && status < 600)
                {
                    pageError = true;
                    pageErrorCode = status;
                }

                if (status != 304)
                {
                    requestsNotFromCache++;
                    if (responseSize > 0)
                    {
                        nonCachedSize += responseSize;
                    }
                }
            }

            var pageInfo = new PageInfo
            {
                PageName = pageName,
                PageError = pageError,
                PageErrorCode = pageErrorCode,
                Requests = numberOfRequests,
                RequestsFromCache = numberOfRequests - requestsNotFromCache,
                RequestsNotFromCache = requestsNotFromCache,
                SizeTotal = size,
                SizeFromCache = size - nonCachedSize,
                SizeWithoutCache = nonCachedSize,
                Timings = new PageTimings
                {
                    FirstRequestToLastRequest = totalTime,
                    PageLoaded = onLoad,
                    DomLoaded = onContentLoad,
                    Blocked = blocked,
                    Connect = connect,
                    Dns = dns,
                    Receive = receive,
                    Send = send,
                    Ssl = ssl,
                    Wait = wait,
                },
                Statistics = new PageStatistics
                {
                    CssRequests = css,
                    FlashRequests = flash,
                    HtmlTextRequests = html,
                    ImageRequests = image,
                    JavaScriptRequests = javascript,
                    OthersRequests = others,
                    CssSize = cssSize,
                    FlashSize = flashSize,
                    HtmlTextSize = htmlSize,
                    ImageSize = imageSize,
                    JavaScriptSize = javascriptSize,
                    OthersSize = othersSize,
                },
            };

            return pageInfo;
        }

        /// <summary>
        /// Completes the designated action and captures the resulting page.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <param name="name">The name.</param>
        /// <param name="expectedIndex">If the action generates multiple pages, this indicates which page to capture.</param>
        /// <param name="timeout">The timeout.</param>
        /// <typeparam name="T">The action return type.</typeparam>
        /// <returns>
        /// The return value from the action.
        /// </returns>
        public T DoActionAndCapturePage<T>(Func<T> action, string name, int expectedIndex = 1, int timeout = 2000)
        {
            PrepareForCapture();
            var returnValue = action();
            Capture(name, expectedIndex, timeout);
            return returnValue;
        }

        /// <summary>
        /// Completes the designated action and captures the resulting page.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <param name="name">The name.</param>
        /// <param name="expectedIndex">If the action generates multiple pages, this indicates which page to capture.</param>
        /// <param name="timeout">The timeout.</param>
        public void DoActionAndCapturePage(Action action, string name, int expectedIndex = 1, int timeout = 2000)
        {
            PrepareForCapture();
            action();
            Capture(name, expectedIndex, timeout);
        }

        /// <summary>
        /// Clears the Net Export directory.
        /// </summary>
        private void PrepareForCapture()
        {
            var files = Directory.GetFiles(_config.NetExportDirectory);
            if (files.Length <= 0)
            {
                return;
            }

            foreach (var file in files)
            {
                File.Delete(file);
            }
        }

        /// <summary>
        /// Captures the next HAR export and saves it as <c>name</c>.
        /// </summary>
        /// <param name="name">
        /// The name, this can either be a relative or absolute path, if you omit a path entirely, 
        /// it will be saved to the working directory.
        /// </param>
        /// <param name="expectedIndex">If the action generates multiple pages, this indicates which page to capture.</param>
        /// <param name="timeout">
        /// How long to wait for the HAR export before returning null.
        /// </param>
        private void Capture(string name, int expectedIndex = 1, int timeout = 2000)
        {
            if (!name.EndsWith(".har"))
            {
                name += ".har";
            }

            string latestFile = null;
            const int PollRate = 250;
            int maxTries = timeout / PollRate;
            int tries = 0;

            do
            {
                var files = Directory.GetFiles(_config.NetExportDirectory);
                var latest = DateTime.MinValue;

                if (files.Length >= expectedIndex)
                {
                    foreach (var file in files)
                    {
                        var createTime = File.GetCreationTime(file);
                        if (latest.CompareTo(createTime) < 0)
                        {
                            latest = createTime;
                            latestFile = file;
                        }
                    }
                }

                // If no files were found
                if (string.IsNullOrWhiteSpace(latestFile))
                {
                    Thread.Sleep(250);
                }

                tries++;
            }
            while (string.IsNullOrWhiteSpace(latestFile) && tries < maxTries);

            if (!string.IsNullOrWhiteSpace(latestFile))
            {
                string harFile = name.AbsolutePath();
                var path = Path.GetDirectoryName(harFile);
                if (path != null)
                {
                    Directory.CreateDirectory(path);
                    File.Copy(latestFile, harFile);
                }
            }
        }
    }
}
